package com.packenius.library.xspdf;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.DeflaterOutputStream;

/**
 * Creates a PDF stream from a PDF description object (XSPDF object).
 * @author Christian Packenius, 2013.
 */
class XSCreator {
  /**
   * Current offset in the PDF stream.
   */
  private int currentOffsetInStream = 0;

  /**
   * Number of (reserved) objects.
   */
  int objectCount = 0;

  /**
   * Offset of every object. Unused objects will have offset 0.
   */
  private List<Integer> objectOffsets = new ArrayList<Integer>();

  private final OutputStream out;

  private final XSPDF xsPDF;

  private final XSCreator4PageContent xsCreator4PageContent = new XSCreator4PageContent();

  /**
   * For each alternative font encoding ID this object maps to the object link (e.g. "21 0 R").
   */
  private Map<String, String> fontEncodingObjectLinks = new HashMap<String, String>();

  XSCreator(OutputStream out, XSPDF xsPDF) {
    this.out = out;
    this.xsPDF = xsPDF;
  }

  void create() throws IOException {
    checkDestination();
    if (xsPDF.currentPage != null) {
      xsPDF.currentPage.setPageEnd();
    }
    writeLineToStream("%PDF-" + xsPDF.pdfVersion);
    int catalogObjectID = ++objectCount;
    int pagesObjectID = ++objectCount;
    createCatalogObject(catalogObjectID, pagesObjectID);
    List<Integer> pageObjectIDs = createPagesObject(pagesObjectID);
    createPages(pageObjectIDs, pagesObjectID);
    createXref(catalogObjectID);
    if (xsPDF.pages.isEmpty()) {
      throw new XSPdfException("Created PDF stream has no content!");
    }
  }

  private void checkDestination() {
    Map<XSLink, XSDestination> destinations = xsPDF.destinations;
    for (XSLink link : destinations.keySet()) {
      if (link.destinationName != null) {
        if (destinations.get(link) == null) {
          throw new XSPdfException("Destination <" + link + "> not set for link!");
        }
      }
    }
  }

  private void createCatalogObject(int catalogObjectID, int pagesObjectID) throws IOException {
    String catalog = "/Type/Catalog";
    String pages = "/Pages " + pagesObjectID + " 0 R";
    String pageMode = "/PageMode/" + xsPDF.pageMode.name;
    String pageLabels = getPageLabelString(xsPDF.pages);
    createObject(catalogObjectID, "<<" + catalog + pages + pageMode + pageLabels + ">>");
  }

  private String getPageLabelString(List<XSPage> pages) {
    boolean hasPageLabels = false;
    String s = "";
    for (XSPage xsPage : pages) {
      hasPageLabels |= xsPage.pageLabel != null;
      if (hasPageLabels) {
        if (pages.get(0).pageLabel == null) {
          pages.get(0).pageLabel = new XSPageLabel(null, null, 1);
        }
        break;
      }
    }
    int pageID = -1;
    for (XSPage page : pages) {
      pageID++;
      if (page.pageLabel != null) {
        if (s.isEmpty()) {
          s = "/PageLabels<</Nums[";
        }
        s += pageID + "<<" + page.pageLabel.getPdfContent(xsPDF) + ">>";
      }
    }
    if (!s.isEmpty()) {
      s += "]>>";
    }
    return s;
  }

  private List<Integer> createPagesObject(int pagesObjectID) throws IOException {
    List<Integer> pageObjectIDs = new ArrayList<Integer>();
    String pagesContent = "<</Type/Pages/Count ";
    int pageCount = xsPDF.pages.size();
    pagesContent += pageCount;
    pagesContent += "/Kids[";
    for (int i = 0; i < pageCount; i++) {
      int pageObjectID = ++objectCount;
      xsPDF.pages.get(i).pdfObjectID = pageObjectID;
      pageObjectIDs.add(pageObjectID);
      if (i > 0) {
        pagesContent += " ";
      }
      pagesContent += pageObjectID + " 0 R";
    }
    pagesContent += "]>>";
    createObject(pagesObjectID, pagesContent);
    return pageObjectIDs;
  }

  private void createPages(List<Integer> pageObjectIDs, int pagesObjectID) throws IOException {
    int pageCount = pageObjectIDs.size();
    for (int pageID = 0; pageID < pageCount; pageID++) {
      createPageObject(pageID + 1, pageObjectIDs.get(pageID), xsPDF.pages.get(pageID), pagesObjectID, pageObjectIDs);
    }
  }

  private void createPageObject(int pageID, Integer pageObjectID, XSPage page, int pagesObjectID, List<Integer> pageObjectIDs)
      throws IOException {
    // Create content (and PDF content object) for this page.
    int pageContentObjectID = ++objectCount;
    List<XSAnnotation> annotationObjects = new ArrayList<XSAnnotation>();
    createPageContentObject(pageID, pageContentObjectID, page, annotationObjects);

    // Create page object (page description object).
    double pageWidth = page.pageSize.width;
    double pageHeight = page.pageSize.height;
    // int pageWidth = (int) (xsPage.pageSize.width + .5);
    // int pageHeight = (int) (xsPage.pageSize.height + .5);
    String parent = "/Parent " + pagesObjectID + " 0 R";
    String mediabox = "/MediaBox[0 0 " + pageWidth + " " + pageHeight + "]";
    String content = "/Contents " + pageContentObjectID + " 0 R";
    String resources = "/Resources" + getPageResources(page, pageID);
    String presentation = "";
    if (page.presentationDuration > 0.0) {
      presentation += "/Dur " + page.presentationDuration;
    }
    if (!page.transitionMode.equals(XS.DEFAULT_TRANSITION_MODE)) {
      presentation += page.transitionMode.getPdfContent(xsPDF);
    }
    String annots = createAnnotationArray(annotationObjects, pageObjectIDs);
    String pageContent = "<</Type/Page" + parent + mediabox + content + resources + presentation + annots + " >>";
    createObject(pageObjectID, pageContent);
  }

  private String createAnnotationArray(List<XSAnnotation> annotationObjects, List<Integer> pageObjectIDs) throws IOException {
    if (annotationObjects.isEmpty()) {
      return "";
    }
    String s = "/Annots [";
    for (XSAnnotation annotation : annotationObjects) {
      int annotationObjectID = ++objectCount;
      s += " " + annotationObjectID + " 0 R";
      annotation.setPdfObjectID(annotationObjectID);
      createObject(annotationObjectID, annotation.getPdfContent(xsPDF));
    }
    return s + " ]";
  }

  private String getPageResources(XSPage page, int pageID) throws IOException {
    String pageResources = "<</Font<<" + getPageFonts(page) + " >>";
    String imageResources = getImageResources(page, pageID);
    if (imageResources != null && imageResources.length() > 0) {
      pageResources += "/XObject<<" + imageResources + ">>";
    }
    pageResources += " >>";
    return pageResources;
  }

  private String getImageResources(XSPage page, int pageID) throws IOException {
    String s = "";
    int imageIndex = 0;

    Set<Integer> imageIDs = new HashSet<Integer>();

    for (XSColumn column : page.columns) {
      for (XSImage image : column.images) {
        int imageObjectID = createImageStreamObject(image, imageIndex++, pageID, page);
        if (imageIDs.add(imageObjectID)) {
          s += "/Im" + image.imageID + " " + imageObjectID + " 0 R";
        }
      }
    }
    return s.trim();
  }

  private int createImageStreamObject(XSImage image, int imageIndex, int pageID, XSPage page) throws IOException {
    String imageID = image.imageID;
    Integer imageObjectID = page.xsPDF.imageObjectIDs.get(imageID);
    if (imageObjectID == null) {
      imageObjectID = ++objectCount;
      page.xsPDF.imageObjectIDs.put(imageID, imageObjectID);
      byte[] bytes = page.xsPDF.images.get(imageID);
      if (bytes == null) {
        URL imageURL = page.xsPDF.imageURLs.get(Integer.parseInt(imageID.substring(3)));
        createExternalImageStreamObject(image, imageObjectID, imageURL);
      }
      else {
        createInternalImageStreamObject(image, imageObjectID, bytes);
      }
    }
    return imageObjectID;
  }

  private void createExternalImageStreamObject(XSImage image, Integer imageObjectID, URL imageURL) throws IOException {
    // <</F<</F(http://pia.provinzial.com/intranet/file/8ab38e203b937fdd013c1aa96394218a.de.0/ma+fotos+ausweise+70x70.jpg)/FS/URL>>>>
    String sImageUrl = imageURL.toString();
    if (sImageUrl.startsWith("file:/") && !sImageUrl.startsWith("file:///")) {
      sImageUrl = "file:///" + sImageUrl.substring(6);
    }
    String imageDict =
      "<</Subtype/Image/Length 0/FFilter/DCTDecode/BitsPerComponent 8/ColorSpace/DeviceRGB/Width " + image.realPixelWidth
        + "/Height " + image.realPixelHeight + "/Type/XObject/F<</FS/URL/F("
        + XSStatics.escapeStandardStringCharacters(sImageUrl) + ")>>>>\rstream\r";
    byte[] imageObjectBytes = XSStatics.concat(imageDict.getBytes(), new byte[0], "\rendstream".getBytes());
    createObject(imageObjectID, imageObjectBytes);
  }

  private void createInternalImageStreamObject(XSImage image, Integer imageObjectID, byte[] bytes) throws IOException {
    String imageDict = "<</Subtype/Image/Length " + bytes.length + "/Filter/DCTDecode"
      + "/BitsPerComponent 8/ColorSpace/DeviceRGB/Width " + image.realPixelWidth + "/Height "
      + image.realPixelHeight + "/Type/XObject>>\rstream\r";
    byte[] imageObjectBytes = XSStatics.concat(imageDict.getBytes(), bytes, "\rendstream".getBytes());
    createObject(imageObjectID, imageObjectBytes);
  }

  private String getPageFonts(XSPage page) throws IOException {
    Set<XSFontTypeAndEncodingInformation> fonts = page.usedFontTypesAndEncodings;
    if (fonts.isEmpty()) {
      return "";
    }
    String fontsContent = "";
    for (XSFontTypeAndEncodingInformation fontTAEI : fonts) {
      fontsContent += " " + getFontDictionary(fontTAEI);
    }
    return fontsContent.substring(1);
  }

  private String getFontDictionary(XSFontTypeAndEncodingInformation fontTAEI) throws IOException {
    XSAlternativeFontEncoding fontEncoding = fontTAEI.fontEncoding;
    XSFontType fontType = fontTAEI.fontType;
    String encodingID = "", encodingObjRef = "";
    if (fontEncoding != null) {
      encodingID = fontEncoding.getID();
      encodingObjRef = getEncodingObjectLink(fontEncoding);
    }
    String fullFontName = "/" + fontType.logicalName + encodingID;
    return fullFontName + " <</Type/Font/Subtype/Type1/BaseFont/" + fontType.fontName + encodingObjRef + ">>";
  }

  private String getEncodingObjectLink(XSAlternativeFontEncoding fontEncoding) throws IOException {
    String altFontEncID = fontEncoding.getID();
    if (fontEncodingObjectLinks.get(altFontEncID) == null) {
      int encodingObjectID = ++objectCount;
      String objectContent = "<< /Type/Encoding/Differences [" + fontEncoding.getPdfDifferencesListContent() + "] >>";
      createObject(encodingObjectID, objectContent);
      fontEncodingObjectLinks.put(altFontEncID, encodingObjectID + " 0 R");
    }
    return "/Encoding " + fontEncodingObjectLinks.get(altFontEncID);
  }

  private void createPageContentObject(int pageID, int pageContentObjectID, XSPage page, List<XSAnnotation> annotationObjects)
      throws IOException {
    String pageContentAsString = xsCreator4PageContent.getPageContentString(page, this, annotationObjects);
    switch (page.xsPDF.contentEncoding) {
    case NoEncoding:
      generatePageContentWithoutEncoding(pageContentObjectID, pageContentAsString);
      break;
    case DeflateEncoding:
      generatePageContentWithDeflateEncoding(pageContentObjectID, pageContentAsString);
      break;
    }
  }

  private void generatePageContentWithoutEncoding(int pageContentObjectID, String pageContentAsString)
      throws IOException {
    int pageStreamLength = pageContentAsString.getBytes().length;
    pageContentAsString = "<</Length " + pageStreamLength + ">>\rstream\r" + pageContentAsString + "\rendstream";
    createObject(pageContentObjectID, pageContentAsString);
  }

  private void generatePageContentWithDeflateEncoding(int pageContentObjectID, String pageContentAsString)
      throws IOException {
    ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
    DeflaterOutputStream deflaterOut = new DeflaterOutputStream(byteArrayOut);
    deflaterOut.write(pageContentAsString.getBytes());
    pageContentAsString = null;
    deflaterOut.close();
    byte[] streamContentAsByteArray = byteArrayOut.toByteArray();
    int streamContentLength = streamContentAsByteArray.length;
    byte[] objectContent = XSStatics.concat(
      ("<</Filter/FlateDecode/Length " + streamContentLength + ">>\rstream\r").getBytes(), streamContentAsByteArray,
      "\rendstream".getBytes());
    streamContentAsByteArray = null;
    createObject(pageContentObjectID, objectContent);
  }

  private void createObject(int objectID, String objectContent) throws IOException {
    createObject(objectID, objectContent.getBytes());
  }

  private void createObject(int objectID, byte[] objectContent) throws IOException {
    while (objectOffsets.size() <= objectID) {
      objectOffsets.add(0);
    }
    objectOffsets.set(objectID, currentOffsetInStream);
    writeLineToStream(objectID + " 0 obj");
    writeToStream(objectContent);
    writeNewLine();
    writeLineToStream("endobj");
  }

  private void createXref(int catalogObjectID) throws IOException {
    int xrefOffset = currentOffsetInStream;
    writeLineToStream("xref");
    writeLineToStream("0 " + (1 + objectCount));
    writeToStream("0000000000 65535 f\r\n");
    for (int i = 1; i <= objectCount; i++) {
      writeToStream(tenth(objectOffsets.get(i)) + " 00000 n\r\n");
    }
    writeLineToStream("trailer");
    writeLineToStream("<</Size " + (1 + objectCount) + "/Root " + catalogObjectID + " 0 R>>");
    writeLineToStream("startxref");
    writeLineToStream("" + xrefOffset);
    writeLineToStream("%%EOF");
  }

  private String tenth(int k) {
    String s = "0000000000" + k;
    return s.substring(s.length() - 10);
  }

  private void writeLineToStream(String line) throws IOException {
    writeToStream(line);
    writeNewLine();
  }

  private void writeToStream(String line) throws IOException {
    byte[] bytes = line.getBytes();
    writeToStream(bytes);
  }

  private void writeToStream(byte[] bytes) throws IOException {
    out.write(bytes);
    currentOffsetInStream += bytes.length;
  }

  private void writeNewLine() throws IOException {
    out.write('\r');
    currentOffsetInStream++;
  }
}
